package com.cognitran.cssvalidator.ant.taskdefs;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.w3c.css.util.ApplContext;

import com.cognitran.cssvalidator.ant.output.formatters.OutputFormatter;
import com.cognitran.cssvalidator.ant.types.Format;
import com.cognitran.cssvalidator.ant.types.Output;
import com.cognitran.cssvalidator.validation.CSSValidator;
import com.cognitran.cssvalidator.validation.model.CSSValidationResults;

/**
 * A task that executes a validation for a given CSS profile (and optionally medium).
 * 
 * @author garethf
 */
public class CSSProfileValidatorSubTask extends Task
{
	
    /** A locale used for uppercase conversion - en_US is safe, as no non-ASCII characters are expected. */
    private static final Locale US_LOCALE = new Locale("en", "US");
  
    
	/** File sets for this profile that should be validated. */
	private final List<FileSet> fileSets = new ArrayList<FileSet>();
	
	/** The CSS profile type to validate against. */
	private Profile type = Profile.CSS21;
	
	/** The CSS profile type. */
	private Medium medium = Medium.ALL;
	
	
	
	private String warningLevel;
	
	

	
	

    /** The output format type collection. */
    private Output output = null;
	
  
    
    /** A flag indicating if the task should output debug information. */
    private boolean isDebugEnabled;
	
    /** A flag indicating if the task should fail if warnings are found. */
    private boolean failOnWarning;
	
    /** A flag indicating if the task should fail if errors are found. */
    private boolean failOnError;
    
	
	/**
	 * Add a <code>FileSet</code> <Code>Type</code> to be processed by this task.
	 * 
	 * @param fileSet A file set from the task declaration that should be used when executing this task.
	 */
	public void addFileSet(final FileSet fileSet)
	{
		fileSets.add(fileSet);
	}
	
	
	/**
	 * Set the CSS profile to be used.
	 * 
	 * @param tag The string value for the CSS profile.
	 */
	public void setType(final String tag)
	{
		if (tag == null)
		{
			throw new BuildException("CSS profile 'type' attribute value is mandatory, allowed values are: " + Profile.tagsAsString());
		}

		try
		{
			this.type = Profile.fromTag(tag.toLowerCase(US_LOCALE));
		}
		catch (IllegalArgumentException iae)
		{
			throw new BuildException("Unknown CSS profile '" + tag + "' in type attribute, allowed values are: " + Profile.tagsAsString());
		}
	}
	
	
	/**
	 * Set the CSS medium to be used.
	 * 
	 * @param tag The string value for the CSS medium.
	 */
	public void setMedium(final String tag)
	{
		if (tag == null)
		{
			throw new BuildException("CSS profile 'medium' attribute value is mandatory, allowed values are: " + Medium.tagsAsString());
		}

		try
		{
			this.medium = Medium.fromTag(tag.toLowerCase(US_LOCALE));
		}
		catch (IllegalArgumentException iae)
		{
			throw new BuildException("Unknown CSS medium '" + tag + "' in medium attribute, allowed values are: " + Medium.tagsAsString());
		}
	}

	
	
	
	
	
	public void setWarningLevel(final String warningLevel)
	{
		this.warningLevel = warningLevel;
	}
	
	
	
	
	
	
	

    
    /**
     * Set the output instance that will be used.
     * 
     * @param output The output instance that will drive formatted output.
     */
    public void addConfiguredOutput(final Output output)
    {
        this.output = output;
    }


    /**
     * Configure the task before it is run.
     * 
     * @param isDebugEnabled Is debug enabled whilst executing this task.
     * @param failOnWarning Should this task fail the build if a warning is found.
     * @param failOnError Should this task fail the build if an error is found.
     */
	public void configure(final boolean isDebugEnabled, final boolean failOnWarning, final boolean failOnError) 
	{
		this.isDebugEnabled = isDebugEnabled;
		this.failOnWarning = failOnWarning;
		this.failOnError = failOnError;
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void execute() throws BuildException 
	{
        log("Validating CSS using profile '" + type.getTag() + "', medium '" + medium.getTag() + "'");

		// Build a CSS parsing context, and validator for later use.
		final ApplContext appContext = buildApplicationContext();
		final CSSValidator validator = new CSSValidator(appContext);
		
		// For each registered output format, create a formatter
		final List<OutputFormatter> formatters = new ArrayList<OutputFormatter>();
		for (final Format format : output.getOutputFormats())
		{
			final OutputFormatter formatter = format.getFormatter();
			if (isDebugEnabled)
			{
				log("Registering output format '" + format.getType() + "' with formatter class [" + formatter.getClass().getSimpleName() + "]");
				log("Formatter output location is -> " + output.getDir());
			}

			formatter.setOutputLocation(output.getDir());
			formatter.init();
			
			formatters.add(formatter);
		}
		
		log (" ");
		for (final FileSet fileSet : fileSets)
		{
			// Each file set gives up a set of qualifying files.
			final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject());
			final File baseDir = scanner.getBasedir();
			
			final String[] files = scanner.getIncludedFiles();
			for (String file : files)
			{
				// For each included file ... check eligibility, and parse.
				final File source = new File(baseDir, file);
				if (isDebugEnabled)
				{
					log("Parsing - " + source.toString());
				}
				
				try 
				{
					// Validate each located CSS resource.
					final CSSValidationResults results = validator.validate(source);
					
					// If the build should fail on error and/or warning, set the markers.
					if (failOnError && results.hasErrors())
					{
						 
					}
					if (failOnWarning && results.hasWarnings())
					{
						 
					}
                    
					
                    

					
			        // For each registered output formatter ... write the validated css output.
			        for (final OutputFormatter formatter : formatters)
			        {
						if (isDebugEnabled)
						{
							log("  Formatting results using '" + formatter.getClass().getSimpleName() + "'.");
						}

			        	formatter.writeFormattedOutput(source, results);
			        }
				}
				catch (Exception ef)
				{
					// If there is any kind of problem with the validation tasks (from W3C), just bail.
					throw new BuildException("General Error whilst validating CSS, message was -> " + ef.getMessage(), ef);
				}
			}
		}
		
        // For each registered output formatter ... request that they cleanup.
        for (final OutputFormatter formatter : formatters)
        {
        	formatter.cleanup();
        }
	}
	

	/**
	 * Build an application context based on supplied input to parse css files.
	 * 
	 * @return An application context that represents the supplied input.
	 */
	private ApplContext buildApplicationContext()
	{
		// Create an application context in the required language for the output.
		final ApplContext appContext = new ApplContext(output.getLanguage());     

		// Work which profile to use.
        if (!(Profile.NONE == type))
        {
        	
        	if (Profile.CSS1 == type || Profile.CSS2 == type     || Profile.CSS21 == type || Profile.CSS3 == type || 
        		Profile.SVG == type  || Profile.SVGBASIC == type || Profile.SVGTINY == type)
        	{
        		// These CSS profiles, just set the CSS version.
				appContext.setCssVersion(type.getTag());
        	}
        	else
        	{
        		// Use the given profile, and the default CSS version.
	        	appContext.setProfile(type.getTag());
				appContext.setCssVersion(Profile.CSS21.getTag());
        	}
        }
        else
        {
        	// If the profile was set to none, use default CSS version.
        	appContext.setProfile(type.getTag());
			appContext.setCssVersion(Profile.CSS21.getTag());
        }
		
		// Set the required medium.
		appContext.setMedium(medium.getTag());
		
		return appContext;
	}
	

	/**
	 * An enumeration of valid CSS profiles for validation.
	 *  
	 * @author garethf
	 */
	public static enum Profile
	{
		/** Pure CSS related profiles. */
		CSS1("css1"), CSS2("css2"), CSS21("css21"), CSS3("css3"), 
		
		/** SVG related profiles. */
		SVG("svg"), SVGBASIC("svgbasic"), SVGTINY("svgtiny"), 
		
		/** Mobile/TV related profiles. */
		ATSCTV("atsc-tv"), MOBILE("mobile"), TV("tv"),
		
		/** The special none profile. */
		NONE("none");
		
		
		/** The tag name used for the profile. */
		private final String tag;
		
		
		/**
		 * Create a CSS profile with an associated tag.
		 * 
		 * @param tag The tag associated with this CSS profile.
		 */
		private Profile(final String tag)
		{
			this.tag = tag;
		}
		

		/**
		 * Get the tag associated with this CSS profile.
		 * 
		 * @return The tag associated with this CSS profile.
		 */
		public String getTag()
		{
			return tag;
		}

		
		/**
		 * Retrieve a CSS profile from a given tag.
		 * 
		 * @param tag The CSS profile tag to find a profile from.
		 */
		public static Profile fromTag(final String tag)
		{
			for (Profile profile : Profile.values())
			{
				if (profile.getTag().equals(tag))
				{
					return profile;
				}
			}
			
			throw new IllegalArgumentException("Unknown CSS medium tag.");
		}
		
		
		/**
		 * Get a string containing the (comma separated) list of valid tag values.
		 * 
		 * @return A comma separated String that lists valid values for this enumeration.
		 */
		public static String tagsAsString()
		{
			final StringBuilder b = new StringBuilder();
			for (Profile profile : Profile.values())
			{
				if (b.length() > 0)
				{
					b.append(", ");
				}
				
				b.append(profile.getTag());
			}
			
			return b.toString();
		}
	}
	
	
	/**
	 * An enumeration of valid CSS mediums for validation.
	 *  
	 * @author garethf
	 */
	public static enum Medium
	{
		/** Mediums that are available to validate against. */
		ALL("all"), AURAL("aural"), BRAILLE("braille"), EMBOSSED("embossed"), 

		HANDHELD("handheld"), PRINT("print"), PROJECTION("projection"), SCREEN("screen"), 
		
		TTY("tty"), TV("tv"), PRESENTATION("presentation");
		
		
		/** The tag name used for the medium. */
		private final String tag;
		
		
		/**
		 * Create a CSS medium with an associated tag.
		 * 
		 * @param tag The tag associated with this CSS medium.
		 */
		private Medium(final String tag)
		{
			this.tag = tag;
		}
		

		/**
		 * Get the tag associated with this CSS medium.
		 * 
		 * @return The tag associated with this CSS medium.
		 */
		public String getTag()
		{
			return tag;
		}

		
		/**
		 * Retrieve a CSS medium from a given tag.
		 * 
		 * @param tag The CSS medium tag to find a medium from.
		 */
		public static Medium fromTag(final String tag)
		{
			for (Medium medium : Medium.values())
			{
				if (medium.getTag().equals(tag))
				{
					return medium;
				}
			}
			
			throw new IllegalArgumentException("Unknown CSS medium tag.");
		}	
		
		
		/**
		 * Get a string containing the (comma separated) list of valid tag values.
		 * 
		 * @return A comma separated String that lists valid values for this enumeration.
		 */
		public static String tagsAsString()
		{
			final StringBuilder b = new StringBuilder();
			for (Medium medium : Medium.values())
			{
				if (b.length() > 0)
				{
					b.append(", ");
				}
				
				b.append(medium.getTag());
			}
			
			return b.toString();
		}
	}

}
